package flca.mda.api.util;

import static flca.mda.api.util.OutputOption.EXCLUDE_ID;
import static flca.mda.api.util.OutputOption.EXCLUDE_MANYTOMANY;
import static flca.mda.api.util.OutputOption.EXCLUDE_MANYTOONE;
import static flca.mda.api.util.OutputOption.EXCLUDE_ONETOMANY;
import static flca.mda.api.util.OutputOption.EXCLUDE_ONETOONE;
import static flca.mda.api.util.OutputOption.EXCLUDE_RELATIONS;
import static flca.mda.api.util.OutputOption.EXCLUDE_VAL;
import static flca.mda.api.util.OutputOption.EXCLUDE_VAR;
import static flca.mda.api.util.OutputOption.GIVE_VALUE;
import static flca.mda.api.util.OutputOption.WITH_COMMON;
import static flca.mda.api.util.OutputOption.WITH_DATATYPE;
import static flca.mda.api.util.OutputOption.WITH_DEFAULT;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.zip.ZipFile;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import mda.annotation.scala.Val;
import mda.type.IEntityType;
import mda.type.IServiceType;
import reactive.Tid;
import flca.mda.codegen.CodegenConstants;
import flca.mda.codegen.data.DataStore;
import flca.mda.codegen.helpers.AnnotationsHelper;
import flca.mda.codegen.helpers.ShellUtils;
import flca.mda.codegen.helpers.ZipFileHelper;
import flca.mda.common.api.helpers.ImportHelper;

public class ScalaTypeUtils extends ReactiveTypeUtils {

	public static Logger logger = LoggerFactory.getLogger(ScalaTypeUtils.class);

	// common val's
	public static String FD = "fd"; // fetchDepth
	public static String OHC = "ohc"; // old rw hash code
	public static String OPTION = "Option[";
	public static String SOME = "Some(";
	public static String NONE = "None";
	public static String FETCH = "Fd";
	public static String ENUM_VALUE = ".Value";
	public static String ISDEFINED = ".isDefined";
	public static String ROW = "row";
	public static String OBJ = "obj";
	public static String QUERY = "Query";
	public static String RETRIEVE = "retrieve";
	public static String ROUTE = "Route";
	public static String IS = " === ";
	// --

	private static final Map<String, String> COMMON_VALS;
	private static final Map<String, String> COMMON_VAL_DEFAULTS;
	private static final String ID_DEFAULT = NONE;

	static {
		COMMON_VALS = new HashMap<String, String>();
		COMMON_VALS.put(FD, null); // this one is filled at runtime
		COMMON_VALS.put(OHC, "List[Long]");
		COMMON_VAL_DEFAULTS = new HashMap<String, String>();
		COMMON_VAL_DEFAULTS.put(FD, NONE);
		COMMON_VAL_DEFAULTS.put(OHC, "List(0)");
	}

	/**
	 * this a list of common "val" variables. Currently "fd" and "ohc"
	 * 
	 * @return
	 */
	public List<String> getCommonVals() {
		List<String> result = new ArrayList<String>();
		result.add(FD);
		result.add(OHC);
		return result;
	}

	/**
	 * return all parameter names only as a comma seperated string of the
	 * current class example: "id, fd, ohc"
	 * 
	 * @return String
	 */
	public String getAllParams() {
		return getAllParams(currentClass());
	}

	/**
	 * return all parameter names only as a comma seperated string of the
	 * current class example: "id, fd, ohc"
	 * 
	 * @return String
	 */
	public String getAllParams(Class<?> aClass) {
		return getAllParams(currentClass(), new OutputOption[] {});
	}

	/**
	 * return all parameter names only as a comma seperated string of the
	 * current class example: "id, fd, ohc"
	 * 
	 * @return String
	 */
	public String getAllParams(OutputOption[] outputOpts) {
		return getAllParams(currentClass(), outputOpts);
	}

	/**
	 * return all parameter names in a comma seperated string, of the given
	 * class depending on the options, the Type and default value is supplied as
	 * well example: outputoption = {WITH_DATATYPE, WITH_DEFAULT}
	 * "id:Some(Long)=None, fd:Some(FetchModel)=None, ohc:Long=0"
	 * 
	 * @return String
	 */
	public String getAllParams(Class<?> aClass, OutputOption[] outputOpts) {
		StringBuffer sb = new StringBuffer();

		if (!hasOutputOption(EXCLUDE_ID, outputOpts)) {
			sb.append(getIdValParam(outputOpts));
		}

		if (hasOutputOption(WITH_COMMON, outputOpts)) {
			sb.append(getCommonValParams(aClass, outputOpts));
		}

		for (Field fld : getAllFields(aClass)) {
			if (getThisValOrVarParams(fld, outputOpts)) {
				importIfNeeded(fld);
				if (hasOutputOption(OutputOption.WITH_PREFIX_VAL, outputOpts))
					sb.append("val ");
				if (hasOutputOption(OutputOption.WITH_PREFIX_VAR, outputOpts))
					sb.append("var ");
				sb.append(fld.getName());
				if (hasOutputOption(OutputOption.WITH_PARAMNAME, outputOpts))
					sb.append(fld.getName() + "=");

				String typ = getTypeName(fld);
				if (hasOutputOption(OutputOption.WITH_OPTION, outputOpts))
					typ = option(typ, !isRequired(fld));
				if (hasOutputOption(WITH_DATATYPE, outputOpts))
					sb.append(":" + typ);

				if (hasOutputOption(WITH_DEFAULT, outputOpts)) {
					sb.append("=" + getDefaultValue(fld));
				} else if (hasOutputOption(GIVE_VALUE, outputOpts)) {
					sb.append("=" + fld.getName());
				}

				sb.append(", ");
			}
		}
		return nu.trimRightComma(sb.toString());
	}

	private boolean getThisValOrVarParams(Field aField,
			OutputOption[] outputOpts) {
		if (isIdField(aField))
			return false;
		if (isVal(aField)
				&& hasOutputOption(OutputOption.EXCLUDE_VAL, outputOpts))
			return false;
		if (isVar(aField)
				&& hasOutputOption(OutputOption.EXCLUDE_VAR, outputOpts))
			return false;
		if (isRelation(aField)
				&& hasOutputOption(OutputOption.EXCLUDE_RELATIONS, outputOpts))
			return false;
		if (isOneToOneField(aField)
				&& hasOutputOption(OutputOption.EXCLUDE_ONETOONE, outputOpts))
			return false;
		if (isOneToManyField(aField)
				&& hasOutputOption(OutputOption.EXCLUDE_ONETOMANY, outputOpts))
			return false;
		if (isManyToOneField(aField)
				&& hasOutputOption(OutputOption.EXCLUDE_MANYTOONE, outputOpts))
			return false;
		if (isManyToManyField(aField)
				&& hasOutputOption(OutputOption.EXCLUDE_MANYTOMANY, outputOpts))
			return false;
		return true;
	}

	public String getEntityCaseClassParams() {
		StringBuffer sb = new StringBuffer();
		OutputOption opts[] = new OutputOption[] { OutputOption.WITH_COMMON,
				OutputOption.EXCLUDE_VAL, OutputOption.EXCLUDE_VAR,
				OutputOption.WITH_DATATYPE, OutputOption.WITH_DEFAULT,
				OutputOption.WITH_PREFIX_VAL };
		sb.append(getAllParams(opts) + ", ");

		OutputOption defopts[] = new OutputOption[] {
				OutputOption.WITH_DATATYPE, OutputOption.WITH_DEFAULT,
				OutputOption.EXCLUDE_ID, OutputOption.EXCLUDE_RELATIONS,
				OutputOption.WITH_OPTION };
		opts = addOutputOption(defopts, OutputOption.EXCLUDE_VAR,
				OutputOption.WITH_PREFIX_VAL);
		String s2 = getAllParams(opts);
		if (!s2.trim().isEmpty()) {
			sb.append(s2 + ", ");
		}
		opts = addOutputOption(defopts, OutputOption.EXCLUDE_VAL,
				OutputOption.WITH_PREFIX_VAR);
		sb.append(getAllParams(opts));
		return nu.trimChars(sb.toString(), ",");
	}

	public String getDtoCaseClassParams() {
		StringBuffer sb = new StringBuffer();
		OutputOption opts[] = new OutputOption[] { OutputOption.EXCLUDE_ID,
				OutputOption.EXCLUDE_VAL, OutputOption.EXCLUDE_VAR,
				OutputOption.WITH_DATATYPE, OutputOption.WITH_DEFAULT,
				OutputOption.WITH_PREFIX_VAL };
		sb.append(getAllParams(opts) + ", ");

		OutputOption defopts[] = new OutputOption[] {
				OutputOption.WITH_DATATYPE, OutputOption.WITH_DEFAULT,
				OutputOption.EXCLUDE_ID, OutputOption.EXCLUDE_RELATIONS,
				OutputOption.WITH_OPTION };
		opts = addOutputOption(defopts, OutputOption.EXCLUDE_VAR,
				OutputOption.WITH_PREFIX_VAL);
		String s2 = getAllParams(opts);
		if (!s2.trim().isEmpty()) {
			sb.append(s2 + ", ");
		}
		opts = addOutputOption(defopts, OutputOption.EXCLUDE_VAL,
				OutputOption.WITH_PREFIX_VAR);
		sb.append(getAllParams(opts));
		return nu.trimChars(sb.toString(), ",");
	}

	public OutputOption[] addOutputOption(OutputOption[] opts,
			OutputOption... addOpt) {
		OutputOption[] result = new OutputOption[opts.length + addOpt.length];
		for (int i = 0; i < opts.length; i++) {
			result[i] = opts[i];
		}
		for (int i = 0; i < addOpt.length; i++) {
			result[opts.length + i] = addOpt[i];
		}

		return result;
	}

	/**
	 * TODO naar TypeUtils verplaatsen returns all Fields in FieldWrapper from
	 * the given class depending on the OutputOptions
	 * 
	 * @param aClass
	 * @return
	 */
	public List<FieldWrapper> getFields(Class<?> aClass,
			OutputOption[] outputOpts) {
		List<FieldWrapper> result = new ArrayList<FieldWrapper>();

		if (!hasOutputOption(EXCLUDE_ID, outputOpts)
				&& !hasOutputOption(EXCLUDE_VAL, outputOpts)
				&& !hasExplicitId(aClass)) {
			result.add(new FieldWrapper(new MockField(Long.class, ID, true)));
		}

		for (Field fld : getAllFields(aClass)) {
			if (!skipField(fld, outputOpts)) {
				importIfNeeded(fld);
				result.add(new FieldWrapper(fld));
			}
		}

		if (hasOutputOption(WITH_COMMON, outputOpts)) {
			result.add(new FieldWrapper(new MockField(Long.class, OHC, false)));
			result.add(new FieldWrapper(new MockField(FetchDepth.class, FD,
					false)));
		}

		return result;
	}

	/**
	 * rteurn true if this field should be skipped base on the OutputOptions
	 * 
	 * @param fld
	 * @param opts
	 * @return
	 */
	private boolean skipField(Field fld, OutputOption[] outputOpts) {
		if (hasOutputOption(EXCLUDE_ID, outputOpts)) {
			if (isIdField(fld))
				return true;
		}
		if (hasOutputOption(EXCLUDE_VAL, outputOpts)) {
			if (isVal(fld) || isIdField(fld))
				return true;
		}
		if (hasOutputOption(EXCLUDE_VAR, outputOpts)) {
			if (isVar(fld))
				return true;
		}
		if (hasOutputOption(EXCLUDE_ONETOMANY, outputOpts)
				|| hasOutputOption(EXCLUDE_RELATIONS, outputOpts)) {
			if (isOneToManyField(fld))
				return true;
		}
		if (hasOutputOption(EXCLUDE_ONETOONE, outputOpts)
				|| hasOutputOption(EXCLUDE_RELATIONS, outputOpts)) {
			if (isOneToOneField(fld))
				return true;
		}
		if (hasOutputOption(EXCLUDE_MANYTOONE, outputOpts)
				|| hasOutputOption(EXCLUDE_RELATIONS, outputOpts)) {
			if (isManyToOneField(fld))
				return true;
		}
		if (hasOutputOption(EXCLUDE_MANYTOMANY, outputOpts)
				|| hasOutputOption(EXCLUDE_RELATIONS, outputOpts)) {
			if (isManyToManyField(fld))
				return true;
		}
		return false;
	}

	/**
	 * return all parameter names in a comma seperated string, of the given
	 * class, that will be used for Row classes
	 * 
	 * @return String
	 */
	public String getRowCaseClassParams(Class<?> aClass) {
		StringBuffer sb = new StringBuffer();
		getRowCaseClassParamsExclOneTo(aClass, sb);
		getRowCaseClassParamsOneToOne(aClass, sb);
		return nu.trimRightComma(sb.toString());
	}

	private void getRowCaseClassParamsExclOneTo(Class<?> aClass, StringBuffer sb) {
		for (FieldWrapper fldw : getFields(aClass, new OutputOption[] {
				EXCLUDE_ONETOMANY, EXCLUDE_ONETOONE })) {
			importIfNeeded(fldw);
			sb.append(rowFieldname(fldw));
			String dtype = rowDatatype(fldw);
			sb.append(":" + dtype);
			if (fldw.isId()) {
				sb.append("=" + getDefaultRowValue(fldw));
			}
			sb.append(", ");
		}
	}

	private void getRowCaseClassParamsOneToOne(Class<?> aClass, StringBuffer sb) {
		for (Field fld : getAllOneToOneFields(aClass)) {
			importIfNeeded(fld);
			sb.append(rowFieldname(fld));
			String dtype = getIdTypeName(fld.getType());
			sb.append(":" + OPTION + dtype + "]");
			sb.append("=" + NONE);
			sb.append(", ");
		}
	}

	/**
	 * return all parameter names in a comma seperated string, of the given
	 * class, that will be used for Row copy method
	 * 
	 * @return String
	 */
	public String getRowCloneParams(Class<?> aClass) {
		StringBuffer sb = new StringBuffer();

		for (FieldWrapper fldw : getFields(aClass,
				new OutputOption[] { EXCLUDE_ONETOMANY })) {
			sb.append("this." + rowFieldname(fldw));
			sb.append(", ");
		}
		return nu.trimRightComma(sb.toString());
	}

	private static final String ENUMSTR_OPT = "val <%=P1%> = if (obj.<%=P1%>.isDefined) Some(obj.<%=P1%>.get.toString()) else None";
	private static final String ENUMSTR_REQ = "val <%=P1%> = obj.<%=P1%>.toString() ";

	/**
	 * return all parameter names in a comma seperated string, of the given
	 * class, that will be used for Row copy method ex:CRow(obj.id, obj.cname,
	 * obj.getAId)
	 * 
	 * @return String
	 */
	public String getMapToRowCode(Class<?> aClass, String aRow) {
		StringBuffer sb = new StringBuffer();

		for (Field fld : getAllEnums(aClass)) {
			Properties props = new Properties();
			props.put("P1", fld.getName());
			String str = (isRequired(fld)) ? nu.substitute(ENUMSTR_REQ, props)
					: nu.substitute(ENUMSTR_OPT, props);
			sb.append("   " + str + NL);
		}
		sb.append("    " + aRow + "(");

		for (FieldWrapper fldw : getFields(aClass,
				new OutputOption[] { EXCLUDE_RELATIONS })) {
			if (fldw.isEnum()) {
				sb.append(fldw.getName());
			} else if (fldw.getField() != null && isDate(fldw.getField())) {
				sb.append(some("toSqlDate(" + OBJ + "." + fldw.getName() + ")",
						!fldw.isRequired()));
			} else {
				sb.append(OBJ + "." + fldw.getName());
			}
			sb.append(", ");
		}

		for (Field fld : getAllManyToOneAndOneToOneFields(aClass)) {
			String fldname = (isEntity(fld)) ? fld.getName() + ID_SUFFIX : fld
					.getName();
			sb.append(OBJ + "." + fldname);
			sb.append(", ");
		}

		return nu.trimRightComma(sb.toString()) + ")";
	}

	/**
	 * return all parameter names in a comma seperated string, of the given
	 * class, that will be used for the make() def in the Mock class
	 * 
	 * @return String
	 */
	public String getMockMakeParams(Class<?> aClass) {
		StringBuffer sb = new StringBuffer();

		for (FieldWrapper fldw : getFields(aClass, new OutputOption[] {})) {
			sb.append(fldw.getName());
			String typename = getTypeName(fldw);
			String dtype = null;
			if (fldw.isOneToManyField()) {
				dtype = typename;
			} else {
				dtype = option(typename, (fldw.isId() || !fldw.isRequired()));
			}

			sb.append(":" + dtype);
			sb.append("=" + getDefaultValue(fldw));
			sb.append(", ");
		}

		return nu.trimRightComma(sb.toString());
	}

	/**
	 * returns the slick projection string to (un)map a sql query to a row ex:
	 * def * = (id, cname, a) <> (CRow.tupled, CRow.unapply)
	 * 
	 * @param aClass
	 * @return
	 */
	public String getRowProjectionParams(Class<?> aClass) {
		StringBuffer sb = new StringBuffer();

		getRowProjectionParamsExclOneTo(aClass, sb);
		getRowProjectionParamsOneToOne(aClass, sb);
		return nu.trimRightComma(sb.toString());
	}

	private void getRowProjectionParamsExclOneTo(Class<?> aClass,
			StringBuffer sb) {
		for (FieldWrapper fldw : getFields(aClass, new OutputOption[] {
				EXCLUDE_ONETOMANY, EXCLUDE_ONETOONE })) {
			String fldname = rowFieldname(fldw);
			sb.append(fldname);
			if (fldw.isId()) {
				sb.append(".?");
			}
			;
			sb.append(", ");
		}
	}

	private void getRowProjectionParamsOneToOne(Class<?> aClass, StringBuffer sb) {
		for (Field fld : getAllOneToOneFields(aClass)) {
			String fldname = rowFieldname(fld);
			sb.append(fldname);
			sb.append(", ");
		}
	}

	/**
	 * returns the slick row fieldname
	 * 
	 * @param fldw
	 * @return
	 */
	public String rowFieldname(FieldWrapper fldw) {
		return fldw.isManyToOneField() ? fldw.getName() : fldw.getName();
	}

	/**
	 * @see rowFieldname(FieldWrapper fldw)
	 * @param fld
	 * @return
	 */
	public String rowFieldname(Field fld) {
		FieldWrapper fldw = new FieldWrapper(fld);
		return rowFieldname(fldw);
	}

	/**
	 * return the name of the methode name used to get the pk of a manytoone
	 * object ex: picture_id
	 * 
	 * @param fld
	 * @return
	 */
	public String defNameGetPk(Field fld) {
		return fld.getName() + ID_SUFFIX;
	}

	/**
	 * returns the slick row field datatype
	 * 
	 * @param fldw
	 * @return
	 */
	public String rowDatatype(FieldWrapper fldw) {
		if (fldw.isManyToOneField()) {
			return (fldw.isRequired()) ? "Long" : OPTION + "Long" + "]";
		} else {
			return getRowTypeName(fldw);
		}
	}

	/**
	 * @see rowDatatype(FieldWrapper fldw)
	 * @param fld
	 * @return
	 */
	public String rowDatatype(Field fld) {
		FieldWrapper fldw = new FieldWrapper(fld);
		return rowDatatype(fldw);
	}

	private boolean hasOutputOption(OutputOption checkOption,
			OutputOption[] outputOpts) {
		for (OutputOption opt : outputOpts) {
			if (opt.equals(checkOption))
				return true;
		}
		return false;
	}

	private String getIdValParam(OutputOption[] outputOpts) {
		StringBuffer sb = new StringBuffer();
		if (hasOutputOption(OutputOption.WITH_PREFIX_VAL, outputOpts))
			sb.append("val ");
		sb.append(getIdName());
		if (hasOutputOption(WITH_DATATYPE, outputOpts))
			sb.append(":" + OPTION + getIdTypeName() + "]");
		if (hasOutputOption(WITH_DEFAULT, outputOpts))
			sb.append("=" + ID_DEFAULT);
		if (hasOutputOption(GIVE_VALUE, outputOpts))
			sb.append("=" + getIdName());
		sb.append(", ");
		return sb.toString();
	}

	private String getCommonValParams(Class<?> aClass, OutputOption[] outputOpts) {
		StringBuffer sb = new StringBuffer();
		for (String name : COMMON_VALS.keySet()) {
			String dtype = null;
			if (name.equals(FD)) {
				dtype = OPTION + FETCH + aClass.getSimpleName() + "]";
			} else {
				dtype = COMMON_VALS.get(name);
			}
			String defaultValue = COMMON_VAL_DEFAULTS.get(name);
			if (hasOutputOption(OutputOption.WITH_PREFIX_VAL, outputOpts))
				sb.append("val ");
			sb.append(name);
			if (hasOutputOption(WITH_DATATYPE, outputOpts))
				sb.append(":" + dtype);
			if (hasOutputOption(WITH_DEFAULT, outputOpts))
				sb.append("=" + defaultValue);
			if (hasOutputOption(GIVE_VALUE, outputOpts))
				sb.append("=" + name);
			sb.append(", ");
		}
		return sb.toString();
	}

	/**
	 * This returns all the code to find out if a (nested) object should be
	 * fetched. ex: val fetchOrderOrderStatus = fd.isDefined &&
	 * fd.get.order.isDefined && fd.get.order.get.orderStatus.isDefined This
	 * code recursive loops through all (deeply) nested objects
	 * 
	 * @param aClass
	 * @return
	 */
	public String makeIsFetchDefinedStr(Class<?> aClass) {
		StringBuffer sb = new StringBuffer();
		for (NestedObject nested : getNestedObjects(aClass).getNestedObjects()) {
			sb.append(makeIsFetchDefinedStr(nested) + NL);
		}
		return sb.toString();
	}

	/*
	 * private def fetchUserPicture(fd:Option[FetchUser]) = fd.isDefined &&
	 * fd.get.fetchPicture.isDefined TODO check for endless recursion
	 */
	private String makeIsFetchDefinedStr(NestedObject aNestedObj) {

		Class<?> rootclz = aNestedObj.getRootClass();
		Class<?> currclz = aNestedObj.getNested();
		String clsname = currclz.getSimpleName();
		impu.addScalaImport(FETCH + clsname, currclz, Tid.SCALA_ENTITY.name());

		String fetchname = makeDefNameFetch(aNestedObj);
		String ifstmt = FD + ISDEFINED + " && "; // "fd.isDefined && "
		String fieldsSoFar = FD;
		for (int i = 0; i < aNestedObj.getParents().size(); i++) {
			fieldsSoFar += ".get." + aNestedObj.getFields().get(i).getName();
			ifstmt += fieldsSoFar + ISDEFINED;
			if (i < aNestedObj.getParents().size()-1) {
				ifstmt += " && ";
			}
		}
		
		String fdtype = "(" + FD + ":" + OPTION + FETCH + rootclz.getSimpleName() + "])";
		String line = "  private def " + fetchname + fdtype + " = " + ifstmt;
		return line;
	}

	/**
	 * this generates the fetch like: "fetchTstATstc" this is used in def
	 * retrieveXxxNestedObjects and as a method itself
	 * 
	 * @param aNestedObj
	 * @return
	 */
	private String makeDefNameFetch(NestedObject aNestedObj) {
		return makeDefNameHelper(aNestedObj, FD);
	}

	private String makeDefNameRetrieve(NestedObject aNestedObj) {
		return makeDefNameRetrieve(aNestedObj.lastParentClass(),
				aNestedObj.lastField());
	}

	private String makeDefNameRetrieve(Class<?> aClass, Field aField) {
		String result = RETRIEVE + aClass.getSimpleName();
		result += nu.capName(aField.getName());
		return result;
	}

	private String makeDefNameHelper(NestedObject aNestedObj, String aSubString) {
		String result = aSubString + nu.capName(aNestedObj.lastParentClass().getSimpleName());
		result += nu.capName(aNestedObj.lastField().getName());
		return result;
	}

	/**
	 * This returns all the code to retrieve all (deeply) nested object if
	 * needed ex: if (fetchUserPicture(fetchDepth)) user.picture =
	 * retrieveUserPicture(user) This code recursive loops through all (deeply)
	 * nested objects
	 * 
	 * @param aClass
	 * @return
	 */
	public String genRetrieveNestedObjectsStr(Class<?> aClass) {
		StringBuffer sb = new StringBuffer();
		for (NestedObject nested : getNestedObjects(aClass).getNestedObjects()) {
			sb.append(makeRetrieveNestedObjectsStr(nested) + NL);
			sb.append(S2 + NL);
		}
		return sb.toString();
	}

	/*
	 * if (fetchUserPicture(fd)) user.picture = retrieveUserPicture(user) TODO
	 * check for endless recursion
	 */
	private String makeRetrieveNestedObjectsStr(NestedObject aNestedObj) {
		if (aNestedObj.getFields().size() == 1) {
			return makeRetrieveNestedObjectsStr1(aNestedObj);
		} else {
			return makeRetrieveNestedObjectsStr2(aNestedObj);
		} 
	}

	private String makeRetrieveNestedObjectsStr1(NestedObject aNestedObj) {
		String fldname = aNestedObj.lastField().getName();
		String fetchname = makeDefNameFetch(aNestedObj);
		String retrieveStr = makeDefNameRetrieve(aNestedObj);
		String headOpt = (isOneToOneField(aNestedObj.lastField())) ? ".headOption" : "";
		String line = NL + S4 + "if (" + fetchname + "(fd)) " + OBJ + "." + fldname
				+ " = " + retrieveStr + "(" + OBJ + ")" + headOpt;
		return line;
	}

	private String makeRetrieveNestedObjectsStr2(NestedObject aNestedObj) {
		StringBuffer sb = new StringBuffer();
		String fetchname = makeDefNameFetch(aNestedObj);
		String retrieveStr = makeDefNameRetrieve(aNestedObj);
		Field lastFld = aNestedObj.lastField();
		String lastFldname = lastFld.getName();
		String lastFldType = getGenericOrActualType(lastFld).getSimpleName();
		Field mappedByFld = (hasMappedByFkField(lastFld)) ? getMappedByFkField(lastFld)	: null;
		
		sb.append(S4 + "if (" + fetchname + "(fd)) {" + NL);
		for (int i=1; i<aNestedObj.getParents().size(); i++) {
			Field prevFld = aNestedObj.getFields().get(i-1);
			String prevFldname = prevFld.getName();
			String prevFldType = getGenericOrActualType(prevFld).getSimpleName();
			sb.append(makeRetrieveNestedObjectsStr3(prevFldname, prevFldType, i) + NL);
		}

		Field prevFld = aNestedObj.getFields().get(aNestedObj.getParents().size()-2);
		String prevFldname = prevFld.getName();
		String prevFldType = getGenericOrActualType(prevFld).getSimpleName();
		String objItemId = (isOneToManyField(prevFld) && mappedByFld != null) ? getIdName(prevFld .getType()) : lastFldname + ID_SUFFIX;
		String qryItemId = (isOneToManyField(prevFld) && mappedByFld != null) ? mappedByFld.getName()  : getIdName(lastFld.getType());
		if (mappedByFld != null && !isSimpleField(mappedByFld)) {qryItemId += ID_SUFFIX; }
		String headOpt = (isOneToOneField(lastFld)) ? ".headOption" : "";
		int nr = aNestedObj.getParents().size()-1;
		
		sb.append(makeRetrieveNestedObjectsStr4(prevFldname, prevFldType, lastFldname, retrieveStr, 
				lastFldType, qryItemId, objItemId, headOpt, nr) + NL);

		for (int i=aNestedObj.getParents().size()-1; i>0; i--) {
			sb.append(nu.spaces(2*(i+3)) + "})" + NL);
		}

		sb.append(S4 + "}"+NL);
		return sb.toString();
	}

	private static final String RETRIEVE_QUERY_1 = 
			"<%=OBJ%>.<%=P1%>.foreach((objItem<%=NR2%>:<%=P2%>) => {";
// obj.tstcs.foreach((objItem1:Tstc) => {

	private String makeRetrieveNestedObjectsStr3(String fldname, String clzname, int nr) {
		String obj = (nr-1 == 0) ? "obj" : "objItem" + (nr-1);
		Properties props = nu.makeProperties("OBJ", obj, "P1", fldname, "P2", clzname, "NR2", ""+nr);
		StringBuffer sb = new StringBuffer();
		sb.append(nu.spaces(2*(nr+3)) + nu.substitute(RETRIEVE_QUERY_1, props));
		return sb.toString();
	}

	private static final String RETRIEVE_QUERY_2 = 
			"objItem<%=NR1%>.<%=P3%> = <%=P4%>(obj).filter((qryItem<%=NR2%>:<%=P5%>) => {qryItem<%=NR2%>.<%=P6%> == objItem<%=NR1%>.<%=P7%>})<%=P8%>";
//	  ex:
//   if (fdTsteFs(fd)) {
//      obj.tstcs.foreach((objItem1:Tstc) => {
//        objItem1.tstes.foreach( (objItem2:Tste) => {
//           objItem2.fs = retrieveTsteFs(obj).filter((qryItem2:Tstf) => {qryItem2.tsteId == objItem2.id} )
//        })  
//      })
//    }

	private String makeRetrieveNestedObjectsStr4(String prevFldname, String prevFldType, String lastFldname, 
			String retrieveStr, String lastFldType, String qryItemId, String objItemId, String headOpt, int nr) {

		Properties props = nu.makeProperties("NR1", ""+nr, "NR2", ""+(nr+1),
				"P1", prevFldname, "P2", prevFldType, "P3", lastFldname, "P4", retrieveStr, 
				"P5", lastFldType, "P6", qryItemId, "P7", objItemId, "P8", headOpt);
		StringBuffer sb = new StringBuffer();
		sb.append(nu.spaces(2*(nr+4)) + nu.substitute(RETRIEVE_QUERY_2, props));
		return sb.toString();
	}

	public String retrieveNestedObjDef(Class<?> clz, Field fld, Class<?> aType) {
		String type = (aType == null) ? "" : ":" + aType.getSimpleName();
		String result = makeDefNameRetrieve(clz, fld) + "(" + OBJ + type + ")";
		return result;
	}

	/**
	 * Return true if this field us marked with @Val
	 * 
	 * @param fld
	 * @return boolean
	 */
	public boolean isVal(Field fld) {
		return AnnotationsHelper.hasAnnotation(fld, Val.class);
	}

	/**
	 * Return true if this field us marked with @Val
	 * 
	 * @param fld
	 * @return boolean
	 */
	public boolean isVar(Field fld) {
		return !AnnotationsHelper.hasAnnotation(fld, Val.class)
				&& !isIdField(fld);
	}

	/**
	 * Returns the Scala class name for the given Type. Class names will be
	 * added as imports to the GenModel's ImportManager, and the imported form
	 * will be returned.
	 */
	@Override
	public String getTypeName(Class<?> aClass, Class<?> aGenericType) {
		importIfNeeded(aClass, aGenericType);

		if (aClass.isPrimitive()) {
			return ScalaDataTypes.getTypeName(aClass);
		} else if (isCollection(aClass)) {
			if (aGenericType != null) {
				return ScalaDataTypes.getTypeName(aClass) + "["
						+ aGenericType.getSimpleName() + "]";
			} else {
				return ScalaDataTypes.getTypeName(aClass);
			}
		} else if (aClass.isEnum()) {
			return aClass.getSimpleName() + ENUM_VALUE;
		} else {
			return ScalaDataTypes.getTypeName(aClass);
		}
	}

	/**
	 * Returns the Scala class name for the given Type that will be used in
	 * Slick row's
	 */
	public String getRowTypeName(Field fld) {
		FieldWrapper fldw = new FieldWrapper(fld);
		return getRowTypeName(fldw);
	}

	/**
	 * Returns the Scala class name for the given Type that will be used in
	 * Slick row's
	 */
	public String getRowTypeName(FieldWrapper fldw) {

		Class<?> clz = fldw.getType();
		Class<?> genericType = getGenericType(fldw);

		if (fldw.isId()) {
			return OPTION + ScalaDataTypes.getRowTypeName(clz) + "]";
		} else if (clz.isPrimitive()) {
			if (fldw.isRequired()) {
				return ScalaDataTypes.getRowTypeName(clz);
			} else {
				return OPTION + ScalaDataTypes.getRowTypeName(clz) + "]";
			}
		} else if (isCollection(clz)) {
			if (genericType != null) {
				return ScalaDataTypes.getRowTypeName(clz) + "["
						+ genericType.getSimpleName() + "]";
			} else {
				return ScalaDataTypes.getRowTypeName(clz);
			}
		} else if (clz.isEnum()) {
			return option("String", !fldw.isRequired());
		} else {
			return option(ScalaDataTypes.getRowTypeName(clz),
					!fldw.isRequired());
		}

	}

	private void importIfNeeded(FieldWrapper aFieldWrapper) {
		if (aFieldWrapper.getField() != null) {
			importIfNeeded(aFieldWrapper.getField());
		}
		// else we dont need to import anything of MockFields
	}

	private void importIfNeeded(Field aField) {
		importIfNeeded(aField.getType(), aField.getGenericType());
	}

	private void importIfNeeded(Class<?> aClass, Type aGenericType) {
		if (!aClass.isPrimitive()) {
			ImportHelper.addImport(ScalaDataTypes.getImportString(aClass));
		}
		if (aGenericType != null && !aGenericType.equals(aClass)) {
			if (aGenericType instanceof ParameterizedType) {
				ParameterizedType typeimpl = (ParameterizedType) aGenericType;
				for (Type type : typeimpl.getActualTypeArguments()) {
					ImportHelper.addImport(ScalaDataTypes.getImportString(type
							.getClass()));
				}
			}
		}
	}


	/**
	 * This returns something like: fetchOrder:Some(FetchOrder),
	 * fetchPicture:Some(FetchPicture)
	 * 
	 * @param aClass
	 * @return
	 */
	public String getFetchModelParams(OutputOption[] outputOptions) {
		return getFetchModelParams(currentClass(), outputOptions);
	}

	public String getFetchModelParams(Class<?> aClass,
			OutputOption[] outputOptions) {
		StringBuffer sb = new StringBuffer();

		List<Field> fields = getAllOneToManyFields(aClass);
		fields.addAll(getAllManyToOneAndOneToOneFields(aClass));

		for (Field fld : fields) {
			if (isOwner(fld)) {
				sb.append(fld.getName() + ":");
				Class<?> clz = getGenericOrActualType(fld);
				ImportHelper.addImport(clz);
				String typename = FETCH + clz.getSimpleName();
				ImportHelper.addScalaImport(typename, clz, Tid.SCALA_ENTITY.name());
				sb.append(OPTION + typename + "]");
				if (hasOutputOption(WITH_DEFAULT, outputOptions)) {
					sb.append("=" + SOME + typename + "())");
				}
				sb.append(", ");
			}
		}

		return nu.trimRightComma(sb.toString());
	}

	/**
	 * this return something like with OrderDao with OrderStatusDao
	 * 
	 * @return
	 */
	public String getProfileMixins() {
		StringBuffer sb = new StringBuffer();

		Set<Class<?>> allclz = new HashSet<>();

		for (Field fld : getAllRelations(currentClass())) {
			allclz.add(getGenericOrActualType(fld));
			Class<?> ownerMappedByClz = getOwnerClassMappedByFkField(fld);
			if (ownerMappedByClz != null) {
				allclz.add(ownerMappedByClz);
			}
		}

		for (NestedObject nested : getNestedObjects(currentClass())
				.getNestedObjects()) {
			allclz.add(getGenericOrActualType(nested.lastField()));
		}

		for (Class<?> clz : allclz) {
			if (isEntity(clz)) {
				sb.append(" with ");
				String typename = tplu.getClassName(clz, Tid.SCALA_ENTITY_DAO.name());
				sb.append(typename);
			}
		}

		return sb.toString();
	}

	@Override
	/**
	 * Returns the primitive or class name for the given Type. Class names will
	 * be added as imports to the GenModel's ImportManager, and the imported
	 * form will be returned.
	 */
	public String getTypeName(Class<?> aClass) {
		if (!aClass.isPrimitive()) {
			ImportHelper.addImport(ScalaDataTypes.getImportString(aClass));
		}

		return ScalaDataTypes.getTypeName(aClass);
	}

	@Override
	public String getDefaultValue(Field aField) {
		if (isRequired(aField)) {
			return ScalaDataTypes.getDefaultValue(aField.getType());
		} else {
			return NONE;
		}
	}

	@Override
	public String getDefaultValue(FieldWrapper fldw) {
		if (fldw.isId()) {
			return NONE;
		} else if (fldw.isOneToManyField()) {
			return "Set()";
		} else if (fldw.isRequired()) {
			return ScalaDataTypes.getDefaultValue(fldw.getType());
		} else {
			return NONE;
		}
	}

	/**
	 * return a default Row value for the given FieldWrapper
	 * 
	 * @param fldw
	 * @return
	 */
	public String getDefaultRowValue(FieldWrapper fldw) {
		if (!fldw.isRequired() || fldw.isId()) {
			return NONE;
		} else if (fldw.getType().isEnum()) {
			return "null";
		} else if (isManyToOneField(fldw.getField())
				|| isOneToOneField(fldw.getField())) {
			return NONE;
		} else {
			return ScalaDataTypes.getDefaultRowValue(fldw.getType());
		}
	}

	/**
	 * Returns a random value for teh given fieldwraper
	 * 
	 * @param fldw
	 * @return
	 */
	public String getRandomValue(FieldWrapper fldw) {
		if (fldw.getField() != null) {
			return getRandomValue(fldw.getField());
		}
		return "todo";
	}

	/**
	 * Returns a random value for teh given field
	 * 
	 * @param fldw
	 * @return
	 */
	public String getRandomValue(Field fld) {
		String result = null;
		FieldSimpleKind kind = getFieldSimpleKind(fld);

		if (kind.equals(FieldSimpleKind.ENUM)) {
			String name = fld.getType().getSimpleName();
			result = name + ".values.toSeq(intValue(" + name + ".values.size))";
		} else if (kind.equals(FieldSimpleKind.NUMBER)) {
			result = "intValue";
		} else if (kind.equals(FieldSimpleKind.DECIMAL)) {
			result = "doubleValue";
		} else if (kind.equals(FieldSimpleKind.BOOL)) {
			result = "boolValue";
		} else if (kind.equals(FieldSimpleKind.DATETIME)) {
			result = "dateValue";
		} else if (kind.equals(FieldSimpleKind.STRING)) {
			result = "stringValue";
		} else if (isOneToManyField(fld)) {
			Class<?> typ = getGenericOrActualType(fld);
			String mockname = tplu.getClassName(typ, Tid.SCALA_ENTITY_MOCK.name());
			// no Some() here, so we return !!
			return "Set(" + mockname + ".makeRandom(None), " + mockname
					+ ".makeRandom(None))";
		} else if (isManyToOneField(fld) || isOneToOneField(fld)) {
			String mockname = tplu.getClassName(fld.getType(),
					Tid.SCALA_ENTITY_MOCK.name());
			result = mockname + ".makeRandom(None)";
		}

		return some(result, !isRequired(fld));
	}

	@Override
	public String getIdTypeName(Class<?> aClass) {
		Class<?> pktyp = getIdType(aClass);
		return ScalaDataTypes.getTypeName(pktyp);
	}

	/**
	 * Surround the input string with Some( and ) if required
	 * 
	 * @param string
	 * @param required
	 * @return
	 */
	public String some(String string, boolean doSurround) {
		return (doSurround) ? SOME + string + ")" : string;
	}

	/**
	 * Surround the input string with Some( and ) if required
	 * 
	 * @param string
	 * @param required
	 * @return
	 */
	public String option(String string, boolean doSurround) {
		return (doSurround) ? OPTION + string + "]" : string;
	}

	/**
	 * return all the entities as mixins ex:
	 * "with TelcoDao with OrderDao with OrderStatusDao "
	 * 
	 * @return
	 */
	public String getAllDaoMixins() {
		StringBuffer sb = new StringBuffer();
		List<Class<?>> entities = ModelClassesUtils
				.findModelClassesWithInterface(IEntityType.class);
		for (Class<?> entity : entities) {
			String entityDao = tplu.getClassName(entity, Tid.SCALA_ENTITY_DAO.name());
			sb.append(" with " + entityDao);
		}
		return sb.toString();
	}

	/**
	 * return all the entities as mixins ex:
	 * "with TelcoDao with OrderDao with OrderStatusDao "
	 * 
	 * @return
	 */
	public String getAllDaoQueries() {
		StringBuffer sb = new StringBuffer();
		List<Class<?>> entities = ModelClassesUtils
				.findModelClassesWithInterface(IEntityType.class);
		for (Class<?> entity : entities) {
			String rowQuery = nu.uncapName(entity.getSimpleName()) + QUERY;
			sb.append(" " + rowQuery + ".ddl " + " ++");
		}
		return nu.trimRight(sb.toString(), "++");
	}


	/**
	 * this returns all the mixims needed for the receptionist ex: with
	 * ReverseRoute with DaoServicesRoute
	 * 
	 * @param aClass
	 * @return
	 */
	public String getReceptionMixins() {
		StringBuffer sb = new StringBuffer();

		String mixin = tplu.getAppClassName(Tid.SCALA_APP_STATICROUTE.name());
		sb.append(" with " + mixin);
		
		List<Class<?>> entities = ModelClassesUtils.findModelClassesWithInterface(IEntityType.class);
		for (Class<?> entity : entities) {
			if (generateRestService(entity)) {
				mixin = tplu.getClassName(entity, Tid.SCALA_DAO_SERVICE_ROUTE.name());
				sb.append(" with " + mixin);
			}
		}
		List<Class<?>> services = getAllModelServices();
		for (Class<?> srv : services) {
			mixin = tplu.getClassName(srv, Tid.SCALA_SERVICE_ROUTE.name());
			sb.append(" with " + mixin);
		}
		return sb.toString();
	}

	/**
	 * this returns all Actor imports for the receptionist ex: daoServicesRoute
	 * ~ retrieveUserRoute ~ helloRoute
	 * This includes all the entity dao routes and service routes plus the common static route
	 * @param aClass
	 * @return
	 */
	public String getReceptionRoutes() {
		StringBuffer sb = new StringBuffer();
		sb.append("staticRoutes ~" + NL);
		sb.append(getReceptionServiceRoutes() + NL);
		sb.append(getReceptionDaoRoutes() + NL);
		return nu.trimRight(sb.toString(), "~");
	}

	private String getReceptionServiceRoutes() {
		StringBuffer sb = new StringBuffer();
		List<Class<?>> services = ModelClassesUtils.findModelClassesWithInterface(IServiceType.class);
		for (Class<?> srv : services) {
			for (Method m : srv.getMethods()) {
				sb.append(S4 + m.getName() + ROUTE + " ~ " + NL);
			}
		}
		return sb.toString();
	}

	private static final String RECEPTION_ROUTES = S4 + "find<%=ENT%>Route ~ retrieve<%=ENT%>Route ~ save<%=ENT%>Route ~";

	private String getReceptionDaoRoutes() {
		StringBuffer sb = new StringBuffer();
		List<Class<?>> entities = ModelClassesUtils.findModelClassesWithInterface(IEntityType.class);
		for (Class<?> entity : entities) {
			if (generateRestService(entity)) {
				Properties props = new Properties();
				props.put("ENT", entity.getSimpleName());
				String s = nu.substitute(RECEPTION_ROUTES, props);
				sb.append(s + NL);
			}
		}
		return sb.toString();
	}

	// @formatter:off
	/**
	 * 
	 * This generates something like this for all nexted objects in the given
	 * class: private def retrieveTstAtstes(obj:TstA) (implicit session:
	 * Session): Set[TstE] = { val joinQuery = for { tstA <-
	 * tstAQuery if tstA.id === obj.id tstcs <- tstCQuery if tstA.id ===
	 * tstcs.tstaId tste <- tstEQuery if tstcs.id === tste.tstCId } yield (tste)
	 * val rowlist = joinQuery.list rowlist.map(mapFromTstERow(_)).to[Set] }
	 * 
	 * @param aClass
	 * @return
	 */
	// @formatter:on
	public String genRetrieveAndMergeNestedObjects(Class<?> aClass) {
		StringBuffer sb = new StringBuffer();
		for (NestedObject nested : getNestedObjects(aClass).getNestedObjects()) {
			sb.append(genRetrieveAndMergeNestedObjects(nested) + "\n\n");
		}
		return sb.toString();
	}

	private final static String DEF_RETRIEVE_NESTED = "private def <%=P1%>(obj:<%=P2%>) (implicit session: Session) : <%=P3%> = {";

	private String genRetrieveAndMergeNestedObjects(NestedObject aNested) {
		StringBuffer sb = new StringBuffer();
		String rootclz = aNested.getRootClass().getSimpleName();
		String lastfldClz = getGenericOrActualType(aNested.lastField())
				.getSimpleName();
		String rowmapper = "mapFrom" + lastfldClz + "Row";
		String rettyp = "Set[" + lastfldClz + "]";

		Properties props = nu.makeProperties("P1",
				makeDefNameRetrieve(aNested), "P2", rootclz, "P3", rettyp);
		sb.append(S2 + nu.substitute(DEF_RETRIEVE_NESTED, props) + NL);

		sb.append(S4 + "val joinQuery = for {" + NL);
		sb.append(genRetrAndMergeNestedQueries(aNested) + NL);
		sb.append(S4 + "} yield (" + nu.uncapName(lastfldClz) + ")" + NL);
		sb.append(S4 + "val rowlist = joinQuery.list" + NL);
		sb.append(S4 + "rowlist.map(" + rowmapper + "(_)).to[Set]" + NL);
		sb.append(S2 + "}" + NL + NL);
		return sb.toString();
	}

	/**
	 * example: tstA <- tstAQuery if tstA.id === obj.id
	 * 
	 * @param aNested
	 * @return
	 */
	private String genRetrAndMergeNestedQueries(NestedObject aNested) {
		StringBuffer sb = new StringBuffer();
		for (int i = 1; i < aNested.getFields().size(); i++) {
			Class<?> currclz = aNested.getParents().get(i);
			Field currfld = aNested.getFields().get(i);
			Class<?> prevclz = (i > 0) ? aNested.getParents().get(i - 1) : null;
			Field prevfld = (i > 0) ? aNested.getFields().get(i - 1) : null;
			sb.append(genRetrAndMergeNestedQueries(currclz, prevclz, currfld,	prevfld, i));
			sb.append(NL);
		}

		Class<?> currclz = getGenericOrActualType(aNested.lastField());
		Field currfld = aNested.lastField();
		int n = aNested.getParents().size();
		Class<?> prevclz = aNested.getParents().get(n - 1);
		Field prevfld = aNested.getFields().get(n - 1);
		sb.append(genRetrAndMergeNestedQueries(currclz, prevclz, currfld, prevfld, n));

		return sb.toString();
	}

	private String genRetrAndMergeNestedQueries(Class<?> currclz,
			Class<?> prevclz, Field currfld, Field prevfld, int i) {
		StringBuffer sb = new StringBuffer();
		String rowname = nu.uncapName(currclz.getSimpleName());
		String prevrow = (i==1) ? OBJ : nu.uncapName(prevclz.getSimpleName());
		String currfldId = getIdName(currfld.getType());

		if (isOneToOneField(prevfld)) {
			String fldname = (i==1) ? prevfld.getName() + ID_SUFFIX : prevfld.getName();
			sb.append(S6 + rowname + " <- " + rowname + QUERY + " if "
					+ rowname + "." + currfldId + IS + prevrow + "." + fldname);
		} else {
			Field fkfld = getMappedByFkField(prevfld);
			String fkname = (fkfld != null) ? fkfld.getName() : "?fkname?";
			sb.append(S6 + rowname + " <- " + rowname + QUERY + " if "
					+ rowname + "." + fkname + IS + prevrow + "." + currfldId);
		}
		return sb.toString();
	}

	/**
	 * In generated entity classes, fields that denote a onetoone or manytoone
	 * relation also have a cooresponding _xxxId var and xxxId def that holds
	 * the corresponding id of the relation. But manytoone relations, may be
	 * defined as not bi-directional. In this case, this method will return
	 * false otherwise it will return true
	 * 
	 * @param aField
	 * @return
	 */
	public boolean useCorrIdField(Field aField) {
		if (aField == null) {
			return false;
		} else {
			return !isSimpleField(aField);
		}
	}

	/**
	 * In generated entity classes, fields that denote a onetoone or manytoone
	 * relation also have a cooresponding _xxxId var and xxxId def that holds
	 * the corresponding id of the relation. But manytoone relations, may be
	 * defined as not bi-directional. In this case, this method will simply
	 * return the fieldname otherwise it will return something like: "_xxxId"
	 * 
	 * @param aField
	 * @return
	 */
	public String getCorrIdFieldname(Field aField) {
		if (aField == null) {
			return "?corrIdFieldname?";
		} else {
			if (useCorrIdField(aField)) {
				return "_" + aField.getName() + ID_SUFFIX;
			} else {
				return aField.getName();
			}
		}
	}
	
	/**
	 * return a lsit of static prefix paths
	 * @return
	 */
	public List<String> getStaticPaths() {
		List<String> result = new ArrayList<>();
		result.add("\"web\"");
		result.add("\"css\"");
		result.add("\"img\"");
		result.add("\"js\"");
		return result;
	}
	
	/**
	 * This will do a copy + search/replace of a zipfile to setup the target Dart project
	 */
	public void setupScalaProject() {
		try {
			ZipFile zipfile = getScalaProjectZipfile();
			File targetdir = getTargetDir();
			Map<String, String> fromtos = getScalaProjectFromTos();
			ZipFileHelper ziphelper = new ZipFileHelper(zipfile, fromtos, targetdir);
			ziphelper.doGenerate();
		} catch(Exception ex) {
			String msg = "error in setupDartProject " + ex;
			logger.error(msg);
		}
	}

	private File getTargetDir() {
		String target = getSubsvalueFromDataStore("Backend");
		if (target == null) {
			logger.warn("SubsValue 'Backend' not set, will use " + ShellUtils.getTempDir());
			return ShellUtils.getTempDir();
		} else {
			return new File(target);
		}
	}

	private ZipFile getScalaProjectZipfile() throws Exception {
		String cartdir = DataStore.getInstance().getValue(CodegenConstants.DATASTORE_CARTRIDGE_DIR);
		File zipfile = new File(cartdir + ShellUtils.getPathDelim() + "scala_project.zip");
		if (zipfile.exists()) {
			return new ZipFile(zipfile);
		} else {
			throw new Exception("zip file 'scala_project.zip' not found in cartridges folder");
		}
	}

	private Map<String, String> getScalaProjectFromTos() {
		Map<String, String> r = new HashMap<>();
		r.put("APPNAME", getSubsvalueFromDataStore(CodegenConstants.APP_NAME));
		r.put("appname", getSubsvalueFromDataStore(CodegenConstants.APP_NAME).toLowerCase());
		String mainpck = getSubsvalueFromDataStore(CodegenConstants.APP_PACKAGE);
		String maincls = getSubsvalueFromDataStore(CodegenConstants.APP_NAME);
		r.put("MAINCLASS", mainpck + "." + maincls);
		return r;
	}
	
	
}

